package pl.enigmatic.parser.xml;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.Map.Entry;

import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import pl.enigmatic.math.AliasedMap;
import pl.enigmatic.reflection.Methods;
import pl.enigmatic.util.Reflection;
import pl.enigmatic.util.Strings;
import pl.enigmatic.util.XML;
import pl.enigmatic.parser.XMLAccessible;
import pl.enigmatic.parser.XMLAlias;
import pl.enigmatic.parser.XMLConverter;
import pl.enigmatic.parser.metadata.Metadata;
import pl.enigmatic.parser.string.Converter;


public class MapPropertyExtension implements XMLParserExtension {

	static void modify(final Node node, final String item) {
		final NodeList children = node.getChildNodes();
		for (int i = 0; i < children.getLength(); i++) {
			final Node child = children.item(i);
			if (child.getNodeType() == Node.ELEMENT_NODE && child.getNodeName().equals(item)) {
				XML.changeName((Element) child, Metadata.XMLNS, Metadata.ITEM.toString());
			}
		}
	}

	private static class Setter implements PropertySetter {

		/** field representing map property */
		private final Field field;
		/** type of key */
		private final Class<?> key;
		/** type of value */
		private final Class<?> value;
		/** parser for value */
		private final Parser<?> parser;
		/** custom access method */
		private final Method setter;
		private final String item;

		Setter(final Field f, final Converter<?> c, final Method setter) {
			field = f;
			final XMLAccessible ann = field.getAnnotation(XMLAccessible.class);
			key = ann.key();
			value = ann.value();
			parser = value != null ? Parser.createParser(value).copy(c) : null;
			this.setter = setter;
			field.setAccessible(true);
			if (f.isAnnotationPresent(XMLAccessible.Item.class)) {
				item = f.getAnnotation(XMLAccessible.Item.class).value();
			} else {
				item = null;
			}
		}

		private boolean access(final Object target, final Object key, final Object value) {
			try {
				if (setter != null) {
					setter.invoke(target, key, value);
				} else {
					Methods.put.invoke(field.get(target), key, value);
				}
				return true;
			} catch (final Exception e) {}
			return false;
		}

		@Override
		public boolean access(final Object target, final Object value, final ParsingInfo<?> info) {
			return info.containsMetadata(Metadata.KEY) && access(target, info.getMetadata(Metadata.KEY), value);
		}

		@Override
		public String getPropertyName() {
			return field.getName();
		}

		@Override
		public Parser<?> getParser() {
			return parser;
		}
	}

	/** public methods for setting property values */
	private final AliasedMap<String, Setter> setters = new AliasedMap<String, Setter>();
	private final Parser<?> owner;

	MapPropertyExtension(final Parser<?> owner) {
		this.owner = owner;
		final Class<?> clazz = owner.getClazz();
		for (final Entry<String, Field> e : Reflection.getDeclaredFields(clazz).entrySet()) {
			final Field f = e.getValue();
			final String key = e.getKey();
			if (Reflection.isMap(f.getType()) && f.isAnnotationPresent(XMLAccessible.class)) {
				Converter<?> converter = null;
				if (f.isAnnotationPresent(XMLConverter.class)) {
					converter = Converter.create(f.getAnnotation(XMLConverter.class).value());
				}
				Method setter = null;
				for (final Method m : clazz.getDeclaredMethods()) {
					if (m.isAnnotationPresent(XMLAccessible.Setter.class) && m.getAnnotation(XMLAccessible.Setter.class).value().equals(f.getName())) {
						setter = m;
						if (m.isAnnotationPresent(XMLConverter.class)) {
							converter = Converter.create(m.getAnnotation(XMLConverter.class).value());
						}
						break;
					}
				}
				setters.put(key, new Setter(f, converter, setter));
				if (setter != null && setter.isAnnotationPresent(XMLAlias.class)) {
					setters.addAliases(key, setter.getAnnotation(XMLAlias.class).value());
				}
				if (f != null && f.isAnnotationPresent(XMLAlias.class)) {
					setters.addAliases(key, f.getAnnotation(XMLAlias.class).value());
				}
			}
		}
	}

	@Override
	public NodeNameInfo parseNodeName(final Node node, final ParsingInfo<?> info) {
		if (owner.hasContent() && setters.containsKey(owner.getContent())
				&& (Metadata.ITEM.equals(node) || node.getNodeName().equals(setters.get(owner.getContent()).item))) {
			final Setter s = setters.get(owner.getContent());
			return new NodeNameInfo(owner.getContent(), s.value, s.parser);
		} else {
			final String property = owner.getPropertyName(node.getLocalName());
			if (node.getPrefix() == null && !property.contains(".") && Strings.lowerStarted(property)) {
				if (setters.containsKey(property)) {
					final Setter s = setters.get(property);
					modify(node, s.item);
					return new NodeNameInfo(property, s.value, MapParser.createMapParser(s.key, s.value).copy(s.parser));
				}
				return null;
			}
		}
		return null;
	}

	@Override
	public boolean setParsedProperty(final Object target, final ParsingInfo<?> info, final Object value) {
		final Setter s = setters.get(info.getNodeInfo().getProperty());
		final Map<?, ?> map = (Map<?, ?>) value;
		if (s.setter != null) {
			for (final Entry<?, ?> e : map.entrySet()) {
				if (!s.access(target, e.getKey(), e.getValue())) { return false; }
			}
		} else {
			try {
				Methods.putAll.invoke(s.field.get(target), value);
			} catch (final Exception e) {
				return false;
			}
		}
		return true;
	}

	@Override
	public PropertySetter accessor(final String property) {
		return setters.get(property);
	}
}
